你今天註冊了嗎?用後端好朋友 express 跟 middleware 們做一個簡單的會員註冊系統


Posted by Christy on 2022-01-11

本文為 Lidemy [BE201] > [超重要觀念:Middleware] 之 [做一個簡單會員註冊系統] 來學習後端 express 的實作過程,以下摘要內含:實作目標、資料夾結構、基本流程、MVC 功能簡介、使用 bcrypt middleware 加密密碼

零、內容摘要

1. 目標是要做出三個頁面,分別是首頁、註冊及登入,可以寫入且讀取資料庫

2. 資料夾結構

  • controllers

    • user.js: 一個個函式
  • models

    • user.js:跟之前寫 sql query 類似
  • views

    • user folder

      • login.ejs:登入頁面的畫面

      • register.ejs:註冊頁面的畫面

    • index.ejs:畫面的首頁

  • db.js:帳號、密碼,負責連線

  • index.js:負責引入 library 及路由

3. 基本流程:開 table → 寫 models → 寫 views → index.js 寫路由 → 寫 controllers

註:每寫一小段就跑一下看看

4. models: 以前 sql query 的寫法,select or insert into…

5. views: html tag + <%= js 想輸出的東西 %>

6. controllers: 包著一個個函式:

a. 讀取的(.get())就只渲染畫面

login: (req, res) => {
  res.render('user/login')
},

b. 寫入的(.post())裡面拿資料並且處理錯誤

handleLogin: (req, res, next) => {
  const { username, password, nickname } = req.body
  if (!username || !password) {
    req.flash('errorMessage', 'Please fill in all the required fields.')
    return next()
  }
  userModel.get(username, (err, user) => {
    if (err) {
      req.flash('errorMessage', err.toString())
      return next()
    }
    if (!user) {
      req.flash('errorMessage', 'invalid user')
      return next()
    }
    bcrypt.compare(password, user.password, function(err, isSuccess) {
      if (err || !isSuccess) {
        req.flash('errorMessage', 'invalid password')
        return next()
      }
      req.session.username = user.username
      res.redirect('/')
    });
  })
},handleLogin: (req, res, next) => {
  const { username, password, nickname } = req.body
  if (!username || !password) {
    req.flash('errorMessage', 'Please fill in all the required fields.')
    return next()
  }
  userModel.get(username, (err, user) => {
    if (err) {
      req.flash('errorMessage', err.toString())
      return next()
    }
    if (!user) {
      req.flash('errorMessage', 'invalid user')
      return next()
    }
    bcrypt.compare(password, user.password, function(err, isSuccess) {
      if (err || !isSuccess) {
        req.flash('errorMessage', 'invalid password')
        return next()
      }
      req.session.username = user.username
      res.redirect('/')
    });
  })
},

7. 用了 bcrypt 這個 middleware 來加密密碼

一、基本設置

1. sequel pro 開一個叫 users 的 table,包含:

id, username, password, nickname

2. 先寫 model

models > user.js

const db = require('../db')

const userModel = {
  // 傳一個 user 裡面包含所有資料
  add: (user, cb) => {
    db.query(
      'insert into users(username, password, nickname) values(?, ?, ?)',
        [user.username, user.password, user.nickname],
      (err, results) => {
        if (err) return cb(err)
        cb(null)
      });
  },

  get: (username, cb) => {
    db.query(
      'SELECT * from users where username = ?', [username],
      (err, results) => {
        if (err) return cb(err)
        cb(null, results[0])
      });
  }
}

module.exports = userModel

3. 把剛剛 index.js 裡面的 controller 整理一下

// index.js

app.get('/login', userController.login)
app.post('/login', userController.handleLogin)
app.get('/logout', userController.logout)

4. 把剛剛 index.js 裡面的函式放過來 controllers > user

// controllers > user.js

const userModel = require('../models/user')

const userController = {
  get: (req, res) => {

  },

  login: (req, res) => {
    res.render('login')
  },

  handleLogin: (req, res) => {
    if (req.body.password === 'abc') {
      req.session.isLogin = true
      res.redirect('/')
    } else {
      req.flash('errorMessage', 'invalid password')
      res.redirect('/login')
    }
  },

  logout: (req, res) => {
    req.session.isLogin = false
    res.redirect('/')
  }
}

module.exports = userController

5. views > user.js

// index.ejs

<h1>簡易會員系統</h1>

<% if(isLogin) { %>
  Hello, user!
  <a href="logout">logout</a>
<% } else { %>
  Wanna login?
  <a href="/register">register</a>
  <a href="/login">login</a>
<% } %>

要記得在 index.js 渲染畫面才會出現喔

// index.js

app.get('/', (req, res) => {
  res.render('index')
})

二、實作註冊頁面

1. index.js 加上路由

// index.js

app.get('/register', userController.register)
app.post('/register', userController.handleRegister)

2. controllers

error log 1: Error: Route.post() requires a callback function but got a [object Undefined]

錯誤描述:localhost:5001 無法跑起來

解法一:先把這行註解 app.post('/register', userController.handleRegister) ,估計是因為 controller 裡面的 handleRegister 沒有寫好

後來發現是拼錯字啦,吼唷

error log 2: Error: Connection lost: The server closed the connection.

[Nodejs] 解決MySQL Error: Connection lost. The server closed the connection的方法:這個還沒解決,之後再嘗試吧

// controller > user.js

const res = require('express/lib/response')
const userModel = require('../models/user')

const userController = {
  get: (req, res) => {

  },

  login: (req, res) => {
    res.render('login')
  },

  handleLogin: (req, res) => {
    if (req.body.password === 'abc') {
      req.session.isLogin = true
      res.redirect('/')
    } else {
      req.flash('errorMessage', 'invalid password')
      res.redirect('/login')
    }
  },

  register: (req, res) => {
    res.render('user/register')
  },

  handleRegister: (req, res) => {
    const { username, password, nickname } = req.body
    if (!username || !password || !nickname) {
      return req.flash('errorMessage', 'Please fill in all the required fields.')
    }
    bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
        // Store hash in your password DB.
    });
    userModel.add({
      username,
      password,
      nickname
    }, (err) => {
      if (err) {
        return req.flash('errorMessage', err.toString())
      }
      req.session.username = username
      res.redirect('/')
    })
  },

  logout: (req, res) => {
    req.session.username = null
    res.redirect('/')
  }
}

module.exports = userController

3. views 註冊頁面

// views > register.ejs

<h1>register</h1>

<h2><%= errorMessage %></h2>
<form method="POST" action="/register">
  <div>username: <input type="text" name="username" /></div>
  <div>nickname: <input type="text" name="nickname" /></div>
  <div>password: <input type="password" name="password" /></div>
  <input type="submit" />
</form>

4. 密碼加密

a. 參考 node.bcrypt.js 安裝 $ npm install bcrypt

b. 注意這裡要在 controllers > user.js 裡面引入

// controllers > user.js

const bcrypt = require('bcrypt')
const saltRounds = 10

c. 下面程式碼放在 controller > user.js 的 handleRegister 拿到密碼之後

bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
    // Store hash in your password DB.
});
handleRegister: (req, res) => {
  const { username, password, nickname } = req.body
  if (!username || !password || !nickname) {
    return req.flash('errorMessage', 'Please fill in all the required fields.')
  }

  bcrypt.hash(password, saltRounds, function(err, hash) {
    if (err) {
      return req.flash('errorMessage', err.toString())
    }
    userModel.add({
      username,
      password: hash,
      nickname
    }, (err) => {
      if (err) {
        return req.flash('errorMessage', err.toString())
      }
      req.session.username = username
      res.redirect('/')
    })
  });
},

三、實作登入頁面

1. 從路由開始

// index.js

app.get('/login', userController.login)
app.post('/login', userController.handleLogin, redirectBack)
app.get('/logout', userController.logout)

2. controllers 就會呼叫相對應的 models

// controllers > user.js

const userModel = require('../models/user')
const bcrypt = require('bcrypt')
const saltRounds = 10

const userController = {
  login: (req, res) => {
    res.render('login')
  },

  handleLogin: (req, res, next) => {
    const { username, password, nickname } = req.body
    if (!username || !password) {
      req.flash('errorMessage', 'Please fill in all the required fields.')
      return next()
    }
    userModel.get(username, (err, user) => {
      if (err) {
        req.flash('errorMessage', err.toString())
        return next()
      }
      if (!user) {
        req.flash('errorMessage', 'invalid user')
        return next()
      }
      bcrypt.compare(password, user.password, function(err, isSuccess) {
        if (err || !isSuccess) {
          req.flash('errorMessage', 'invalid password')
          return next()
        }
        req.session.username = user.username
        res.redirect('/')
      });
    })
  },

  register: (req, res) => {
    res.render('user/register')
  },

  handleRegister: (req, res, next) => {
    const { username, password, nickname } = req.body
    if (!username || !password || !nickname) {
      req.flash('errorMessage', 'Please fill in all the required fields.')
      return next()
    }

    bcrypt.hash(password, saltRounds, function(err, hash) {
      if (err) {
        req.flash('errorMessage', 'user exists')
        return next()
      }
      userModel.add({
        username,
        password: hash,
        nickname
      }, (err) => {
        if (err) {
          req.flash('errorMessage', 'user exists')
          return next()
        }
        req.session.username = username
        res.redirect('/')
      })
    });
  },

  logout: (req, res) => {
    req.session.username = null
    res.redirect('/')
  }
}

module.exports = userController

3. model 會是這樣

// models > user.js

const db = require('../db')

const userModel = {
  add: (user, cb) => {
    db.query(
      'insert into users(username, password, nickname) values(?, ?, ?)',
      [user.username, user.password, user.nickname],
      (err, results) => {
        if (err) return cb(err)
        cb(null)
      }
    );
  },

  get: (username, cb) => {
    db.query(
      'SELECT * from users where username = ?', [username],
      (err, results) => {
        if (err) return cb(err)
        cb(null, results[0])
      }
    );
  }
}

module.exports = userModel

4. template engine 會自動防止 xss、sql injection 攻擊

  • xss:記得要用的是 <%= username %>,如果是減號 <%- username %> 就不會防止攻擊,輸入什麼就是什麼

  • sql injection:用 ? 代替輸入,'SELECT * from users where username = ?', [username],










Related Posts

Limiting content with specified number of lines

Limiting content with specified number of lines

Angular17 基於 Standalone 專案載入 Material Symbols (Google Icon)

Angular17 基於 Standalone 專案載入 Material Symbols (Google Icon)

當我們在 Google 搜尋時,發生了甚麼事?

當我們在 Google 搜尋時,發生了甚麼事?


Comments